Português

Explore padrões avançados para a Context API do React, incluindo componentes compostos, contextos dinâmicos e técnicas de otimização de performance para gerenciamento de estado complexo.

Padrões Avançados da Context API do React para Gerenciamento de Estado

A Context API do React fornece um mecanismo poderoso para compartilhar estado em sua aplicação sem o 'prop drilling'. Embora o uso básico seja simples, aproveitar todo o seu potencial requer a compreensão de padrões avançados que podem lidar com cenários complexos de gerenciamento de estado. Este artigo explora vários desses padrões, oferecendo exemplos práticos e insights acionáveis para aprimorar seu desenvolvimento com React.

Entendendo as Limitações da Context API Básica

Antes de mergulhar nos padrões avançados, é crucial reconhecer as limitações da Context API básica. Embora adequada para estados simples e globalmente acessíveis, ela pode se tornar complexa e ineficiente para aplicações complexas com estado que muda frequentemente. Todo componente que consome um contexto é renderizado novamente sempre que o valor do contexto muda, mesmo que o componente não dependa da parte específica do estado que foi atualizada. Isso pode levar a gargalos de performance.

Padrão 1: Componentes Compostos com Contexto

O padrão de Componente Composto (Compound Component) aprimora a Context API criando um conjunto de componentes relacionados que compartilham estado e lógica implicitamente através de um contexto. Esse padrão promove a reutilização e simplifica a API para os consumidores. Isso permite que a lógica complexa seja encapsulada com uma implementação simples.

Exemplo: Um Componente de Abas (Tab)

Vamos ilustrar isso com um componente de Abas (Tab). Em vez de passar props por várias camadas, os componentes Tab se comunicam implicitamente através de um contexto compartilhado.

// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabContext = createContext(undefined);

interface TabProviderProps {
  children: ReactNode;
  defaultTab: string;
}

export const TabProvider: React.FC = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const value: TabContextType = {
    activeTab,
    setActiveTab,
  };

  return {children};
};

export const useTabContext = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTabContext must be used within a TabProvider');
  }
  return context;
};

// TabList.js
import React, { ReactNode } from 'react';

interface TabListProps {
  children: ReactNode;
}

export const TabList: React.FC = ({ children }) => {
  return 
{children}
; }; // Tab.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabProps { label: string; children: ReactNode; } export const Tab: React.FC = ({ label, children }) => { const { activeTab, setActiveTab } = useTabContext(); const isActive = activeTab === label; const handleClick = () => { setActiveTab(label); }; return ( ); }; // TabPanel.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabPanelProps { label: string; children: ReactNode; } export const TabPanel: React.FC = ({ label, children }) => { const { activeTab } = useTabContext(); const isActive = activeTab === label; return ( ); };
// Uso
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Conteúdo para a Aba 1
      Conteúdo para a Aba 2
      Conteúdo para a Aba 3
    
  );
}

export default App;

Benefícios:

Padrão 2: Contextos Dinâmicos

Em alguns cenários, você pode precisar de diferentes valores de contexto com base na posição do componente na árvore de componentes ou outros fatores dinâmicos. Contextos dinâmicos permitem que você crie e forneça valores de contexto que variam com base em condições específicas.

Exemplo: Tematização com Contextos Dinâmicos

Considere um sistema de temas onde você deseja fornecer temas diferentes com base nas preferências do usuário ou na seção da aplicação em que ele está. Podemos fazer um exemplo simplificado com temas claro e escuro.

// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = () => {
    setIsDarkTheme(!isDarkTheme);
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme,
  };

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};
// Uso
import { useTheme, ThemeProvider } from './ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    

Este é um componente com tema.

); } function App() { return ( ); } export default App;

Neste exemplo, o ThemeProvider determina dinamicamente o tema com base no estado isDarkTheme. Componentes que usam o hook useTheme serão automaticamente renderizados novamente quando o tema mudar.

Padrão 3: Contexto com useReducer para Estado Complexo

Para gerenciar lógicas de estado complexas, combinar a Context API com useReducer é uma excelente abordagem. useReducer fornece uma maneira estruturada de atualizar o estado com base em ações, e a Context API permite que você compartilhe esse estado e a função de dispatch por toda a sua aplicação.

Exemplo: Uma Lista de Tarefas Simples

// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

interface TodoContextType {
  state: TodoState;
  dispatch: React.Dispatch;
}

const initialState: TodoState = {
  todos: [],
};

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};

const TodoContext = createContext(undefined);

interface TodoProviderProps {
  children: ReactNode;
}

export const TodoProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value: TodoContextType = {
    state,
    dispatch,
  };

  return {children};
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
};
// Uso
import { useTodo, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function AddTodo() { const { dispatch } = useTodo(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Esse padrão centraliza a lógica de gerenciamento de estado dentro do reducer, tornando mais fácil de raciocinar e testar. Os componentes podem despachar ações para atualizar o estado sem precisar gerenciar o estado diretamente.

Padrão 4: Atualizações de Contexto Otimizadas com `useMemo` e `useCallback`

Como mencionado anteriormente, uma consideração chave de performance com a Context API são as renderizações desnecessárias. Usar useMemo e useCallback pode prevenir essas renderizações garantindo que apenas as partes necessárias do valor do contexto sejam atualizadas e que as referências de função permaneçam estáveis.

Exemplo: Otimizando um Contexto de Tema

// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = useCallback(() => {
    setIsDarkTheme(!isDarkTheme);
  }, [isDarkTheme]);

  const value: ThemeContextType = useMemo(() => ({
    theme,
    toggleTheme,
  }), [theme, toggleTheme]);

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};

Explicação:

Sem useCallback, a função toggleTheme seria recriada a cada renderização do ThemeProvider, fazendo com que o value mudasse e acionando renderizações em quaisquer componentes consumidores, mesmo que o tema em si não tivesse mudado. useMemo garante que um novo value só seja criado quando suas dependências (theme ou toggleTheme) mudarem.

Padrão 5: Seletores de Contexto

Seletores de contexto permitem que componentes se inscrevam apenas em partes específicas do valor do contexto. Isso evita renderizações desnecessárias quando outras partes do contexto mudam. Bibliotecas como `use-context-selector` ou implementações personalizadas podem ser usadas para alcançar isso.

Exemplo Usando um Seletor de Contexto Personalizado

// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';

function useCustomContextSelector(
  context: React.Context,
  selector: (value: T) => S
): S {
  const value = useContext(context);
  const [selected, setSelected] = useState(() => selector(value));
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useEffect(() => {
    let didUnmount = false;
    let lastSelected = selected;

    const subscription = () => {
      if (didUnmount) {
        return;
      }
      const nextSelected = latestSelector.current(value);
      if (!Object.is(lastSelected, nextSelected)) {
        lastSelected = nextSelected;
        setSelected(nextSelected);
      }
    };

    // Você normalmente se inscreveria nas mudanças de contexto aqui. Como este é um exemplo
    // simplificado, vamos apenas chamar a inscrição imediatamente para inicializar.
    subscription();

    return () => {
      didUnmount = true;
      // Cancele a inscrição das mudanças de contexto aqui, se aplicável.
    };
  }, [value]); // Re-execute o efeito sempre que o valor do contexto mudar

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplificado por brevidade)
import React, { createContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  setTheme: (newTheme: Theme) => void; 
}

const ThemeContext = createContext(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme: Theme;
}

export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
  const [theme, setTheme] = useState(initialTheme);

  const value: ThemeContextType = {
    theme,
    setTheme
  };

  return {children};
};

export const useThemeContext = () => {
    const context = React.useContext(ThemeContext);
    if (!context) {
        throw new Error("useThemeContext must be used within a ThemeProvider");
    }
    return context;
};

export default ThemeContext;
// Uso
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Fundo
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Cor
; } function App() { const { theme, setTheme } = useThemeContext(); const toggleTheme = () => { setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' }); }; return ( ); } export default App;

Neste exemplo, o BackgroundComponent só é renderizado novamente quando a propriedade background do tema muda, e o ColorComponent só é renderizado novamente quando a propriedade color muda. Isso evita renderizações desnecessárias quando todo o valor do contexto muda.

Padrão 6: Separando Ações do Estado

Para aplicações maiores, considere separar o valor do contexto em dois contextos distintos: um para o estado e outro para as ações (funções de dispatch). Isso pode melhorar a organização do código e a testabilidade.

Exemplo: Lista de Tarefas com Contextos de Estado e Ação Separados

// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

const initialState: TodoState = {
  todos: [],
};

const TodoStateContext = createContext(initialState);

interface TodoStateProviderProps {
  children: ReactNode;
}

export const TodoStateProvider: React.FC = ({ children }) => {
  const [state] = useReducer(todoReducer, initialState);

  return {children};
};

export const useTodoState = () => {
  return useContext(TodoStateContext);
};

// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

const TodoActionContext = createContext | undefined>(undefined);

interface TodoActionProviderProps {
    children: ReactNode;
}

export const TodoActionProvider: React.FC = ({children}) => {
    const [, dispatch] = useReducer(todoReducer, initialState);

    return {children};
};


export const useTodoDispatch = () => {
  const dispatch = useContext(TodoActionContext);
  if (!dispatch) {
    throw new Error('useTodoDispatch must be used within a TodoActionProvider');
  }
  return dispatch;
};

// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};
// Uso
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';

function TodoList() {
  const state = useTodoState();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function TodoActions({ todo }) { const dispatch = useTodoDispatch(); return ( <> ); } function AddTodo() { const dispatch = useTodoDispatch(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

Essa separação permite que os componentes se inscrevam apenas no contexto de que precisam, reduzindo renderizações desnecessárias. Também facilita o teste unitário do reducer e de cada componente isoladamente. Além disso, a ordem do encapsulamento dos providers importa. O ActionProvider deve envolver o StateProvider.

Melhores Práticas e Considerações

Conclusão

A Context API do React é uma ferramenta versátil para gerenciamento de estado. Ao entender e aplicar esses padrões avançados, você pode gerenciar efetivamente estados complexos, otimizar a performance e construir aplicações React mais sustentáveis e escaláveis. Lembre-se de escolher o padrão certo para suas necessidades específicas e de considerar cuidadosamente as implicações de performance do uso do seu contexto.

À medida que o React evolui, o mesmo acontecerá com as melhores práticas em torno da Context API. Manter-se informado sobre novas técnicas e bibliotecas garantirá que você esteja equipado para lidar com os desafios de gerenciamento de estado do desenvolvimento web moderno. Considere explorar padrões emergentes, como o uso de contexto com signals para uma reatividade ainda mais granular.